package zdream.control.region;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import zdream.utils.math.IntRectangle;
import zdream.utils.math.Rectangle;

import static zdream.control.region.Gate.*;
import static zdream.control.region.TerrainConstant.*;
import static zdream.control.region.TerrainConstant.TERRAIN_SOLID;

/**
 * @author Zdream
 * @date 2023-12-24
 * @since 0.0.1
 */
public class RegionBuilder {
	protected Region region;

	public RegionBuilder(String name) {
		this.region = new Region(name);
	}

	public RegionBuilder withRoomPositions(IntRectangle[] rectList) {
		parseRoomPositions(rectList);
		return this;
	}

	private void parseRoomPositions(IntRectangle[] rects) {
		int length = (int) Arrays.stream(rects)
				.filter(Objects::nonNull)
				.count();
		region.rooms = new Room[length];
		int index = 0;

		for (int i = 0; i < rects.length; i++) {
			final IntRectangle rect = rects[i];
			if (rect == null) {
				continue;
			}

			Room room = new Room(region, i);
			region.rooms[index++] = room;

			room.offsetX = rect.x;
			room.offsetY = rect.y;
			room.width = rect.width;
			room.height = rect.height;

			room.terrains = new byte[room.width][room.height];
			room.fillers = new byte[room.width][room.height];
		}
	}

	public RegionBuilder withFieldsSpawnAt(Rectangle positionInRegion, String key) {
		fieldSpawns.add(new FieldBundle(key, positionInRegion));
		return this;
	}

	public RegionBuilder withFieldsSpawnAt(float x, float y, float width, float height, String key) {
		return withFieldsSpawnAt(new Rectangle(x, y, width, height), key);
	}

	public RegionBuilder defineField(String fieldKey, ItemSpec param) {
		fields.add(new ItemBundle(fieldKey, param));
		return this;
	}

	/**
	 * foeKey: "E-1"
	 * foeName: "MM2ShotMan"
	 */
	public RegionBuilder defineFoe(String foeKey, ItemSpec param) {
		foes.add(new ItemBundle(foeKey, param));
		return this;
	}

	public RegionBuilder withFoeSpawnAt(float x, float y, String foeKey) {
		String pointName = "foe-point-" + foeSpawns.size();
		foeSpawns.add(new FoeSpawnBundle(x, y, pointName, foeKey));
		return this;
	}

	public RegionBuilder withFoeSpawnAt(float x, float y, String foeKey, String pointName) {
		foeSpawns.add(new FoeSpawnBundle(x, y, pointName, foeKey));
		return this;
	}

	/**
	 * start point 只能有一个
	 */
	public RegionBuilder withStartPoint(float xInRegion, float yInRegion) {
		this.startPoint = new StartPointBundle(xInRegion, yInRegion);
		return this;
	}

	public RegionBuilder withSavePoint(float xInRegion, float yInRegion, String pointName) {
		this.savePoints.add(new SavePointBundle(xInRegion, yInRegion, pointName));
		return this;
	}

	private static class FieldBundle {
		String key;
		Rectangle positionInRegion;
		FieldBundle(String key, Rectangle positionInRegion) {
			this.key = key;
			this.positionInRegion = positionInRegion;
		}
	}

	private static class ItemBundle {
		String key;
		ItemSpec param;
		ItemBundle(String key, ItemSpec param) {
			this.key = key;
			this.param = param;
		}
		ItemSpec getParam() {
			return param == null ? ItemSpec.empty() : param;
		}
	}

	private static class FoeSpawnBundle {
		float x, y;
		String pointName;
		String foeKey;
		FoeSpawnBundle(float x, float y, String pointName, String foeKey) {
			this.x = x;
			this.y = y;
			this.pointName = pointName;
			this.foeKey = foeKey;
		}
	}

	private static class StartPointBundle {
		float xInRegion, yInRegion;
		public StartPointBundle(float xInRegion, float yInRegion) {
			this.xInRegion = xInRegion;
			this.yInRegion = yInRegion;
		}
	}

	private static class SavePointBundle {
		float xInRegion, yInRegion;
		String pointName;
		public SavePointBundle(float xInRegion, float yInRegion, String pointName) {
			this.xInRegion = xInRegion;
			this.yInRegion = yInRegion;
			this.pointName = pointName;
		}
	}

	protected List<ItemBundle> fields = new ArrayList<>();
	protected List<ItemBundle> foes = new ArrayList<>();
	protected List<FieldBundle> fieldSpawns = new ArrayList<>();
	protected List<FoeSpawnBundle> foeSpawns = new ArrayList<>();
	protected StartPointBundle startPoint;
	protected List<SavePointBundle> savePoints = new ArrayList<>();

	public Region build() {
		initFields();
		initGates();
		initPoints();
		fillWater();
		return region;
	}

	private void initFields() {
		Map<Room, List<FieldSpec>> fieldsMap = new HashMap<>();

		final Room[] rooms = region.rooms;
		for (int i = 0; i < rooms.length; i++) {
			fieldsMap.put(rooms[i], new ArrayList<>());
		}

		final Map<String, ItemSpec> fieldKeyMap = fields.stream()
				.collect(Collectors.toMap(item -> item.key, ItemBundle::getParam));
		for (FieldBundle field : fieldSpawns) {
			final String key = field.key;
			final ItemSpec spec = fieldKeyMap.get(key);

			for (int j = 0; j < rooms.length; j++) {
				float x = field.positionInRegion.x;
				float y = field.positionInRegion.y;
				float w = field.positionInRegion.width;
				float h = field.positionInRegion.height;

				Room room = rooms[j];
				if (room.overlaps(x, y, w, h)) {
					FieldSpec f = new FieldSpec();
					f.name = key;
					f.rectangle = room.transformFromRegionPosition(field.positionInRegion);
					f.spec = spec;
					fieldsMap.get(room).add(f);
				}
			}
		}

		for (Map.Entry<Room, List<FieldSpec>> entry : fieldsMap.entrySet()) {
			final List<FieldSpec> list = entry.getValue();
			if (list.isEmpty()) {
				entry.getKey().fields = new FieldSpec[0];
				continue;
			}

			entry.getKey().fields = list.toArray(new FieldSpec[0]);
		}
	}

	private void initGates() {
		Room[] rooms = region.rooms;
		Map<Room, List<Gate>> gateMap = new HashMap<>();
		for (Room room : rooms) {
			gateMap.put(room, new ArrayList<>());
		}

		for (int i = 0; i < rooms.length; i++) {
			Room room = rooms[i];
			int xStart = room.offsetX; // 看左边界
			int xEnd = xStart + room.width - 1;
			int yStart = room.offsetY;
			int yEnd = yStart + room.height - 1;

			// top
			// TODO 由于要看梯子, 所以这里省略

			for (int j = i + 1; j < rooms.length; j++) {
				Room room2 = rooms[j];

				int xStart2 = room2.offsetX;
				int xEnd2 = xStart2 + room2.width - 1;
				int yStart2 = room2.offsetY;
				int yEnd2 = yStart2 + room2.height - 1;

				List<Gate> gates = null;
				if (xStart - 1 == xEnd2) {
					if (yStart2 < yEnd && yEnd2 > yStart) { // room 的左边界接壤 room2
						gates = createGateHorizontal(room2, room, 0, 0);
					}
				} else if (xEnd + 1 == xStart2) {
					if (yStart2 < yEnd && yEnd2 > yStart) { // room 的右边界接壤 room2
						gates = createGateHorizontal(room, room2, 0, 0);
					}
				} else if (yEnd + 1 == yStart2) {
					if (xStart2 < xEnd && xEnd2 > xStart) { // room 的上边界接壤 room2
						gates = createGateVertical(room2, room, 0, 0);
					}
				} else if (yStart - 1 == yEnd2) {
					if (xStart2 < xEnd && xEnd2 > xStart) { // room 的下边界接壤 room2
						gates = createGateVertical(room, room2, 0, 0);
					}
				}

				if (gates == null) {
					continue;
				}

				gates.forEach(gate -> gateMap.get(gate.srcRoom).add(gate));
			}
		}

		for (Map.Entry<Room, List<Gate>> entry : gateMap.entrySet()) {
			final List<Gate> list = entry.getValue();
			if (list.isEmpty()) {
				continue;
			}

			entry.getKey().gates = list.toArray(new Gate[0]);
		}
	}

	/**
	 * roomL 的右边接壤 roomR, 处理左右接壤的房间, 创建大门的问题
	 * @param roomL
	 *   左边的房间
	 * @param roomR
	 *   右边的房间
	 * @param dx  (dx 和 dy)
	 *   如果将右边的房间从所处的区域挪到左边房间所处的区域 (相当于坐标轴改动),
	 *   那么横纵坐标的变化量. 如果两个房间属于同一个区域, 该值均为 0.
	 */
	private List<Gate> createGateHorizontal(Room roomL, Room roomR, int dx, int dy) {
		int offsetY1 = roomL.offsetY;
		int offsetY2 = roomR.offsetY - dy;

		int yStart = Math.max(offsetY1, offsetY2);
		int yEnd = Math.min(offsetY1 + roomL.height - 1, offsetY2 + roomR.height - 1);

		int x1a = roomL.width - 1;
		int x1b = x1a - 1;
		int x2a = 0;
		int x2b = 1;

		Gate gate1 = new Gate(roomL, roomR), gate2 = new Gate(roomR, roomL);
		gate1.direction = DIRECTION_RIGHT;
		gate2.direction = DIRECTION_LEFT;
		gate1.offset = offsetY2 - offsetY1;
		gate2.offset = offsetY1 - offsetY2;
		gate1.offsetXOfRegion = dx;
		gate1.offsetYOfRegion = dy;
		gate2.offsetXOfRegion = -dx;
		gate2.offsetYOfRegion = -dy;

		List<Integer> ia1 = new ArrayList<>(yEnd - yStart + 1);
		List<Integer> ia2 = new ArrayList<>(yEnd - yStart + 1);

		for (int y = yStart; y <= yEnd; y++) { // y 是绝对高度
			// 计算房间内部的高度
			int y1 = y - offsetY1;
			int y2 = y - offsetY2; // y - offsety2 是相对于 rooml 所在区域的

			if (Terrains.isEmpty(roomL.terrains[x1a][y1]) && Terrains.isEmpty(roomR.terrains[x2a][y2])) {
				if (Terrains.isEmpty(roomR.terrains[x2b][y2])) {
					ia1.add(y1);
				}
				if (Terrains.isEmpty(roomL.terrains[x1b][y1])) {
					ia2.add(y2);
				}
			}
		}

		List<Gate> gates = new ArrayList<>();
		if (ia1.size() > 0) {
			gate1.exits = ia1.stream().mapToInt(i -> i).toArray();
			gates.add(gate1);
		}
		if (ia2.size() > 0) {
			gate2.exits = ia2.stream().mapToInt(i -> i).toArray();
			gates.add(gate2);
		}
		return gates;
	}

	/**
	 * roomT 的下边接壤 roomB, 处理上下接壤的房间, 创建大门的问题
	 * @param roomT
	 *   上边的房间
	 * @param roomB
	 *   下边的房间
	 * @param dx  (dx 和 dy)
	 *   如果将右边的房间从所处的区域挪到左边房间所处的区域 (相当于坐标轴改动),
	 *   那么横纵坐标的变化量. 如果两个房间属于同一个区域, 该值均为 0.
	 */
	private List<Gate> createGateVertical(Room roomT, Room roomB, int dx, int dy) {
		int offsetX1 = roomT.offsetX;
		int offsetX2 = roomB.offsetX - dx;

		int xStart = Math.max(offsetX1, offsetX2);
		int xEnd = Math.min(offsetX1 + roomT.width - 1, offsetX2 + roomB.width - 1);

		int y1a = 0;
		int y1b = 1;
		int y2a = roomB.height - 1;
		int y2b = y2a - 1;

		Gate gate1 = new Gate(roomT, roomB), gate2 = new Gate(roomB, roomT);
		gate1.direction = DIRECTION_BOTTOM;
		gate2.direction = DIRECTION_TOP;
		gate1.offset = offsetX2 - offsetX1;
		gate2.offset = offsetX1 - offsetX2;
		gate1.offsetXOfRegion = dx;
		gate1.offsetYOfRegion = dy;
		gate2.offsetXOfRegion = -dx;
		gate2.offsetYOfRegion = -dy;

		List<Integer> ia1 = new ArrayList<>(xEnd - xStart + 1);
		List<Integer> ia2 = new ArrayList<>(xEnd - xStart + 1);

		for (int x = xStart; x <= xEnd; x++) { // y 是绝对高度
			// 计算房间内部的高度
			int x1 = x - offsetX1;
			int x2 = x - offsetX2;

			if (Terrains.isEmpty(roomT.terrains[x1][y1a]) && Terrains.isEmpty(roomB.terrains[x2][y2a])) {
				if (Terrains.isEmpty(roomB.terrains[x2][y2b])) {
					ia1.add(x1);
				}
				if (Terrains.isEmpty(roomT.terrains[x1][y1b])) {
					ia2.add(x2);
				}
			}
		}

		List<Gate> gates = new ArrayList<>();
		if (ia1.size() > 0) {
			gate1.exits = ia1.stream().mapToInt(i -> i).toArray();
			gates.add(gate1);
		}
		if (ia2.size() > 0) {
			gate2.exits = ia2.stream().mapToInt(i -> i).toArray();
			gates.add(gate2);
		}
		return gates;
	}

	private void initPoints() {
		List<InterestPoint> points = new ArrayList<>();

		// initialization
		if (startPoint != null) {
			final SavePoint point = SavePoint.ofStartPoint(startPoint.xInRegion, startPoint.yInRegion);
			points.add(point);
		}

		for (SavePointBundle bundle : savePoints) {
			final SavePoint point = SavePoint.ofSavePoint(bundle.xInRegion, bundle.yInRegion, bundle.pointName);
			points.add(point);
		}

		final Map<String, ItemSpec> foeKeyMap = foes.stream()
				.collect(Collectors.toMap(foe -> foe.key, ItemBundle::getParam));
		for (FoeSpawnBundle bundle : foeSpawns) {
			ItemSpec spec = foeKeyMap.get(bundle.foeKey);
			if (spec == null) {
				spec = new ItemSpec(bundle.foeKey);
			}
			FoeSpawnPoint point = new FoeSpawnPoint(spec, bundle.pointName, bundle.x, bundle.y);
			points.add(point);
		}

		// collection
		region.points = points.toArray(new InterestPoint[0]);
		Map<Room, List<FoeSpawnPoint>> foePointMap = new HashMap<>();
		for (Room room : region.rooms) {
			foePointMap.put(room, new ArrayList<>());
		}

		for (InterestPoint point : region.points) {
			Room room = region.of(point.xInRegion, point.yInRegion);
			point.setRoom(room);

			if (point.isForFoeSpawn()) {
				foePointMap.get(room).add(point.asFoeSpawn());
			}
		}

		for (Map.Entry<Room, List<FoeSpawnPoint>> entry : foePointMap.entrySet()) {
			final List<FoeSpawnPoint> list = entry.getValue();
			if (list.isEmpty()) {
				entry.getKey().foes = new FoeSpawnPoint[0];
				continue;
			}
			entry.getKey().foes = list.toArray(new FoeSpawnPoint[0]);
		}
	}

	private void fillWater() {
		for (Room room : region.rooms) {
			fillWater(room);
		}
	}

	private void fillWater(Room room) {
		int width = room.width;
		int height = room.height;
		byte[][] terrains = room.terrains;
		byte[][] fillers = room.fillers;

		int startY;
		boolean search;
		for (int x = 0; x < width; x++) {
			startY = 0;
			search = false;

			for (int y = 0; y < height; y++) {
				byte terrain = terrains[x][y];
				switch (terrain) {
					case TERRAIN_WATER_TOP_SURFACE:
						fillWater(fillers[x], startY, y + 1);
						terrains[x][y] = TERRAIN_EMPTY;
						startY = y + 1;
						search = false;
						break;
					case TERRAIN_WATER_BOTTOM_SURFACE:
						terrains[x][y] = TERRAIN_EMPTY;
						if (search) {
							break;
						}
						startY = y;
						search = true;
						break;
					case TERRAIN_WATER:
						fillers[x][y] = FILLER_WATER;
						terrains[x][y] = TERRAIN_EMPTY;
						startY = y;
						search = false;
						break;
					case TERRAIN_SOLID:
						if (search) {
							fillWater(fillers[x], startY, y);
						}
						startY = y + 1;
						search = false;
						break;
				}
			}

			if (search) {
				fillWater(fillers[x], startY, height);
			}
		}
	}

	private void fillWater(byte[] filters, int startY, int endY) {
		Arrays.fill(filters, startY, endY, FILLER_WATER);
	}
}
